﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Xml;
using System.Xml.Linq;

namespace ifunction
{
    public static class XmlReflection
    {
        #region Constants

        /// <summary>
        /// The XML node_ object item
        /// </summary>
        public const string XmlNode_ObjectItem = "Object";

        /// <summary>
        /// The XML node_ method item
        /// </summary>
        public const string XmlNode_MethodItem = "Method";

        /// <summary>
        /// The XML attribute_ type
        /// </summary>
        public const string XmlAttribute_Type = "Type";

        /// <summary>
        /// The XML attribute_ value
        /// </summary>
        public const string XmlAttribute_Value = "Value";

        /// <summary>
        /// The XML attribute_ name
        /// </summary>
        public const string XmlAttribute_Name = "Name";

        /// <summary>
        /// The generic regex
        /// </summary>
        private static Regex genericClassRegex = new Regex(@"(?<TypeName>([\w\.]+`([0-9]+)))\[(?<GenericTypeName>(([\w\.\[\],`]+)))\]", RegexOptions.Compiled);

        /// <summary>
        /// The generic method regex
        /// </summary>
        private static Regex genericMethodRegex = new Regex(@"(?<MethodName>([\w]+))<(?<GenericTypes>(([\w\.\[\],`]+)))>", RegexOptions.Compiled);

        #endregion

        #region public static method

        /// <summary>
        /// XMLs the automatic object.
        /// </summary>
        /// <param name="xmlNode">The XML node.</param>
        /// <param name="assemblies">The assemblies.</param>
        /// <param name="throwException">if set to <c>true</c> [throw exception].</param>
        /// <returns>Object.</returns>
        public static Object XmlToObject(XElement xmlNode, IEnumerable<Assembly> assemblies = null, bool throwException = false)
        {
            if (assemblies == null)
            {
                assemblies = ReflectionExtension.GetAppDomainAssemblies();
            }

            return ParseToObject(xmlNode, assemblies == null ? ReflectionExtension.GetAppDomainAssemblies() : assemblies, throwException);
        }

        /// <summary>
        /// XMLs the automatic object.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="xmlNode">The XML node.</param>
        /// <param name="assemblies">The assemblies.</param>
        /// <param name="throwException">if set to <c>true</c> [throw exception].</param>
        /// <returns>``0.</returns>
        public static T XmlToObject<T>(XElement xmlNode, IEnumerable<Assembly> assemblies = null, bool throwException = false)
        {
            return (T)XmlToObject(xmlNode, assemblies, throwException);
        }

        #endregion

        #region private methods

        /// <summary>
        /// Fills the object, which converts from Xml Node, into the specific object collection.
        /// If the key has existed, then overwrite the object.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="xmlNode">The XML node.</param>
        /// <param name="assemblies">The assemblies.</param>
        /// <param name="throwException">if set to <c>true</c> [throw exception].</param>
        /// <exception cref="System.InvalidOperationException">Failed to FillObjectCollection, Data:  + xmlNode.ToString() + \n\r</exception>
        private static void FillObjectCollection(Dictionary<string, object> container, XElement xmlNode, IEnumerable<Assembly> assemblies, bool throwException = false)
        {

            try
            {
                container.CheckNullObject("container");
                xmlNode.CheckNullObject("xmlNode");
                assemblies.CheckNullObject("assemblies");

                string key = xmlNode.GetAttributeValue(XmlAttribute_Name);
                if (!string.IsNullOrWhiteSpace(key))
                {
                    object obj = ParseToObject(xmlNode);

                    if (container.ContainsKey(key))
                    {
                        container[key] = obj;
                    }
                    else
                    {
                        container.Add(key, obj);
                    }
                }
            }
            catch (Exception ex)
            {
                if (throwException)
                {
                    throw new InvalidOperationException("Failed to FillObjectCollection, Data: " + xmlNode.ToString() + "\n\r", ex);
                }
            }
        }

        /// <summary>
        /// Fills the object by sub nodes.
        /// </summary>
        /// <param name="containerType">Type of the container.</param>
        /// <param name="container">The container.</param>
        /// <param name="objectRootNode">The object root node.</param>
        /// <param name="assemblies">The assemblies.</param>
        /// <param name="throwException">if set to <c>true</c> [throw exception].</param>
        /// <exception cref="System.InvalidOperationException">
        /// Failed to FillObjectProperties. Property is not found for:  + name
        /// or
        /// Failed to FillObjectProperties caused by name of property is null or empty. Xml: + one.ToString() + \n\r
        /// or
        /// </exception>
        private static void FillObjectProperties(Type containerType, object container, XElement objectRootNode, IEnumerable<Assembly> assemblies, bool throwException = false)
        {
            try
            {
                containerType.CheckNullObject("containerType");
                container.CheckNullObject("container");
                objectRootNode.CheckNullObject("objectRootNode");
                assemblies.CheckNullObject("assemblies");

                foreach (var one in objectRootNode.Elements(XmlNode_ObjectItem))
                {
                    string name = one.GetAttributeValue(XmlAttribute_Name);

                    if (!string.IsNullOrWhiteSpace(name))
                    {
                        var propertyInfo = containerType.GetProperty(name);
                        if (propertyInfo != null)
                        {
                            object obj = ParseToObject(one, assemblies, throwException);
                            if (obj != null)
                            {
                                propertyInfo.SetValue(container, obj);
                            }
                        }
                        else
                        {
                            if (throwException)
                            {
                                throw new InvalidOperationException("Failed to FillObjectProperties. Property is not found for: " + name);
                            }
                        }
                    }
                    else
                    {
                        if (throwException)
                        {
                            throw new InvalidOperationException("Failed to FillObjectProperties caused by name of property is null or empty. Xml:" + one.ToString() + "\n\r");
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                if (throwException)
                {
                    throw new InvalidOperationException(string.Format("Failed to FillObjectProperties. Type: [{0}], Xml: [{1}].\n\r", containerType.FullName, objectRootNode.ToString()), ex);
                }
            }
        }

        /// <summary>
        /// Invokes the methods.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="containerObjectXml">The container object XML.</param>
        /// <param name="assemblies">The assemblies.</param>
        /// <param name="throwException">if set to <c>true</c> [throw exception].</param>
        private static void InvokeMethods(object container, XElement containerObjectXml, IEnumerable<Assembly> assemblies, bool throwException = false)
        {
            try
            {
                containerObjectXml.CheckNullObject("containerObjectXml");
                container.CheckNullObject("container");
                assemblies.CheckNullObject("assemblies");

                var methodNodes = containerObjectXml.Elements(XmlNode_MethodItem);
                if (methodNodes != null)
                {
                    foreach (var one in methodNodes)
                    {
                        InvokeMethod(container, one, assemblies, throwException);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException("InvokeMethods", ex);
            }
        }

        /// <summary>
        /// Invokes the method.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="methodNode">The method node.</param>
        /// <param name="assemblies">The assemblies.</param>
        /// <param name="throwException">if set to <c>true</c> [throw exception].</param>
        private static void InvokeMethod(object container, XElement methodNode, IEnumerable<Assembly> assemblies, bool throwException = false)
        {
            try
            {
                methodNode.CheckNullObject("methodNode");
                container.CheckNullObject("container");

                string methodName = methodNode.GetAttributeValue(XmlAttribute_Name);
                if (!string.IsNullOrWhiteSpace(methodName))
                {
                    string[] genericTypeNames;
                    List<Type> genericTypes = new List<Type>();
                    List<object> parameterObjects = new List<object>();

                    if (GetGenericMethod(methodName, out methodName, out genericTypeNames))
                    {
                        foreach (var one in genericTypeNames)
                        {
                            Type type = ReflectionExtension.GetType(one);
                            if (type != null)
                            {
                                genericTypes.Add(type);
                            }
                        }
                    }

                    foreach (var one in methodNode.Elements(XmlNode_ObjectItem))
                    {
                        object obj = ParseToObject(one, assemblies, throwException);
                        if (obj != null)
                        {
                            parameterObjects.Add(obj);
                        }
                    }

                    ReflectionExtension.InvokeMethod(null, container, methodName, parameterObjects.ToArray(), genericTypes.ToArray(), throwException);
                }
            }
            catch (Exception ex)
            {
                if (throwException)
                {
                    throw new InvalidOperationException(string.Format("Failed to invoke method for [{0}] with xml [{1}].", container.GetType().ToString(), methodNode.ToString()), ex);
                }
            }
        }


        #endregion

        #region Util

        /// <summary>
        /// Gets the application assemblies.
        /// </summary>
        /// <returns>IEnumerable{Assembly}.</returns>
        private static IEnumerable<Assembly> GetApplicationAssemblies()
        {
            return AppDomain.CurrentDomain.GetAssemblies();
        }

        /// <summary>
        /// Parses to object from xml.
        /// </summary>
        /// <param name="xmlNode">The XML node.</param>
        /// <param name="assemblies">The assemblies.</param>
        /// <param name="throwException">if set to <c>true</c> [throw exception].</param>
        /// <returns>System.Object.</returns>
        /// <exception cref="System.Exception">Failed to get type for [ + typeFullName.GetStringValue() + ]</exception>
        /// <exception cref="System.NullReferenceException">Xml node is null or empty.</exception>
        private static object ParseToObject(XElement xmlNode, IEnumerable<Assembly> assemblies = null, bool throwException = false)
        {
            object result = null;
            Type objectType = null;
            bool isComplexObject = false;
            bool isGeneric = false;

            try
            {
                xmlNode.CheckNullObject("xmlNode");

                string typeFullName = xmlNode.GetAttributeValue(XmlAttribute_Type);

                if (string.IsNullOrEmpty(typeFullName))
                {
                    return result;
                }

                try
                {
                    objectType = ReflectionExtension.GetType(typeFullName);
                }
                catch (Exception ex)
                {
                    if (throwException)
                    {
                        throw new Exception("Failed to get type for [" + typeFullName.GetStringValue() + "]", ex);
                    }
                    else
                    {
                        return result;
                    }
                }

                if (objectType == null)
                {
                    return result;
                }

                string value = xmlNode.GetAttributeValue(XmlAttribute_Value);
                if (objectType == typeof(string))
                {
                    return value;
                }
                else
                {
                    result = ReflectionExtension.ConvertToObjectByType(objectType, value);

                    if (result == null)
                    {
                        if (xmlNode.HasElements)
                        {
                            isComplexObject = true;
                            isGeneric = objectType.IsGenericType;
                        }
                        else
                        {
                            result = TryCreateObjectWithValue(objectType, xmlNode.GetAttributeValue(XmlAttribute_Value));
                        }
                    }
                }

                if (isComplexObject)
                {
                    try
                    {
                        result = Activator.CreateInstance(objectType);
                    }
                    catch { }

                    if (result != null)
                    {
                        FillObjectProperties(objectType, result, xmlNode, assemblies, throwException);
                        InvokeMethods(result, xmlNode, assemblies, throwException);
                    }
                }
            }
            catch (Exception ex)
            {
                if (throwException)
                {
                    throw new InvalidOperationException("ParseToObject", ex);
                }
            }

            return result;
        }

        /// <summary>
        /// Tries the create object with value.
        /// The method would try to create instance within value as first parameter of constructor. (e.g.: System.Uri)
        /// If failed, then try to create instance via parameterless constructor, and then set <c>Value</c> property.
        /// If failed for all above, return null.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="value">The value.</param>
        /// <returns>The instance or null.</returns>
        public static object TryCreateObjectWithValue(Type type, string value)
        {
            object result = null;

            try
            {
                result = Activator.CreateInstance(type, new object[] { value });
            }
            catch { }

            if (result == null)
            {
                try
                {
                    result = Activator.CreateInstance(type);
                    var valueProperty = type.GetProperty(XmlAttribute_Value);
                    if (valueProperty != null)
                    {
                        valueProperty.SetValue(result, value);
                    }
                }
                catch { }
            }

            return result;
        }





        /// <summary>
        /// Gets the name of the generic type.
        /// </summary>
        /// <param name="fullName">The full name.</param>
        /// <param name="genericTypeNames">The generic type names.</param>
        /// <param name="typeName">Name of the type.</param>
        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
        private static bool GetGenericTypeName(string fullName, out string[] genericTypeNames, out string typeName)
        {
            typeName = string.Empty;
            genericTypeNames = new string[] { };

            var match = genericClassRegex.Match(fullName);
            if (match != null && match.Success)
            {
                genericTypeNames = match.Result("${GenericTypeName}").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                typeName = match.Result("${TypeName}");
                return true;
            }

            return false;
        }

        /// <summary>
        /// Gets the name of the generic type.
        /// </summary>
        /// <param name="fullName">The full name.</param>
        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
        private static bool GetGenericTypeName(string fullName)
        {
            string typeName;
            string[] genericTypeNames;

            return GetGenericTypeName(fullName, out genericTypeNames, out typeName);
        }

        /// <summary>
        /// Gets the generic method.
        /// </summary>
        /// <param name="input">The input.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <param name="genericTypeNames">The generic type names.</param>
        /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
        private static bool GetGenericMethod(string input, out string methodName, out string[] genericTypeNames)
        {
            methodName = input;
            genericTypeNames = new string[] { };

            var match = genericMethodRegex.Match(input);
            if (match != null && match.Success)
            {
                genericTypeNames = match.Result("${GenericTypes}").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
                methodName = match.Result("${MethodName}");
                return true;
            }

            return false;
        }

        #endregion

        #region Get Type


        #endregion
    }
}
